package jeme;

import java.util.HashMap;

import jeme.lang.SchemeObject;
import jeme.lang.SchemeSymbol;

/**
 * Represents a Scheme procedure. A can be applied to arguments in a 
 * given environment. Each procedure is given an integer index, 
 * following Bigloo's strategy for implementing first-class procedures
 * on the JVM. See Serpette and Serrano in 
 * <a href="http://portal.acm.org/citation.cfm?id=581478.581503">Compiling 
 * Scheme to JVM bytecode</a>.
 * 
 * @author Erik Silkensen (silkense@colorado.edu)
 * @version Jun 10, 2009
 */
public abstract class Procedure extends SchemeObject
{
    /**
     * Subclasses may use this format string to raise type errors. 
     * There are three arguments: the string representation of the 
     * invalid argument, the number of the invalid argument, and the 
     * name of the source closure.
     */
    protected static final String TYPE_ERROR_FMT = 
        "The object %s, passed as argument %d to %s, is not the correct type.";
    
    private int min, max, index;
    
    private Environment env;
    
    private HashMap<String, SchemeSymbol> symbols;
    
    /**
     * Creates a procedure with a specified arity and index value. The 
     * default environment is used.
     * 
     * @param arity  the arity of this procedure
     * @param index  the index value of this procedure
     */
    public Procedure(int arity, int index) 
    {
        this(arity, index, null);
    }
    
    /**
     * Creates a procedure with a specified arity, index value, and 
     * environment.
     * 
     * @param arity  the arity of this procedure
     * @param index  the index value of this procedure
     * @param env  the parent of the environment to evaluate this 
     * procedure in
     */
    public Procedure(int arity, int index, Environment env) 
    {
        this(arity, arity, index, env);
    }
    
    /**
     * Creates a procedure with a specified arity range and index 
     * value. The default environment is used. The arity arguments can
     * be used to specify a range of valid argument values (e.g., 
     * <code>new Procedure(1, 2)</code>).
     * 
     * @param arity1  an arity of this procedure
     * @param arity2  an arity of this procedure
     * @param index  the index value of this procedure
     */
    public Procedure(int arity1, int arity2, int index) 
    {
        this(arity1, arity2, index, null);
    }

    /**
     * Creates a procedure with a specified arity range, index value, 
     * and environment. The arity arguments can be used to specify a 
     * range of valid argument values (e.g., <code>new Procedure(1, 
     * 2)</code>).
     * 
     * @param arity1  an arity of this procedure
     * @param arity2  an arity of this procedure
     * @param index  the index value of this procedure
     * @param env  the parent of the environment to evaluate this 
     * procedure in
     */
    public Procedure(int arity1, int arity2, int index, Environment env) 
    {
        setArityMin(Math.min(arity1, arity2));
        setArityMax(Math.max(arity1, arity2));
        setIndex(index);
        setEnvironment(new Environment(env));
        setSymbols(new HashMap<String, SchemeSymbol>());
    }
  
    /**
     * Applies this closure to the specified arguments in this 
     * procedure's environment.
     * 
     * @param args  the arguments to the procedure
     * @return  the result of the application
     */
    public abstract SchemeObject apply(SchemeObject... args);
    
    /**
     * Should be called in compiled procedures as they enter. This 
     * adjusts the "stack frame" (environment bindings).
     */
    public void enter()
    {
        setEnvironment(new Environment(env));
    }
    
    /**
     * Should be called in compiled procedures as they exit. This 
     * adjusts the "stack frame" (environment bindings) and removes 
     * symbol instances that were specific to this procedure.
     */
    public void exit()
    {
        setEnvironment(env.getParent());
        removeSymbols();
    }

    /**
     * Returns the index value of this procedure.
     * 
     * @return  the index value of this procedure
     */
    public int getIndex() 
    {
        return index;
    }

    /**
     * Returns the minimum arity of this procedure.
     * 
     * @return  the minimum arity of this procedure
     */
    public int getArityMin() 
    {
        return min;
    }

    /**
     * Returns the maximum arity of this procedure.
     * 
     * @return  the maximum arity of this procedure
     */
    public int getArityMax() 
    {
        return max;
    }

    /**
     * Returns the environment to evaluate this procedure in.
     * 
     * @return  the environment to evaluate this procedure in
     */
    public Environment getEnvironment() 
    {
        return env;
    }
    
    /**
     * Sets the index value of this procedure.
     * 
     * @param index  the index value of this procedure
     */
    protected void setIndex(int index) 
    {
        this.index = index;
    }

    /**
     * Sets the minimum arity of this procedure.
     * 
     * @param min  the minimum arity of this procedure
     */
    protected void setArityMin(int min) 
    {
        this.min = min;
    }

    /**
     * Sets the maximum arity of this procedure.
     * 
     * @param min  the maximum arity of this procedure
     */
    protected void setArityMax(int max) 
    {
        this.max = max;
    }

    /**
     * Sets the environment to evaluate this procedure in.
     * 
     * @param env  the environment to evaluate this procedure in
     */
    protected void setEnvironment(Environment env) 
    {
        this.env = env;
    }
    
    /**
     * Sets the symbols used in this procedure.
     * 
     * @param symbols  the symbols used in this procedure
     */
    protected void setSymbols(HashMap<String, SchemeSymbol> symbols)
    {
        this.symbols = symbols;
    }
    
    /**
     * Returns whether or not a given arity is valid for this 
     * procedure.
     * 
     * @param arity  an arity to test
     * @return  <code>true</code> is the arity is valid, else 
     * <code>false</code>
     */
    protected boolean isValidArity(int arity) 
    {
        return arity >= getArityMin() && arity <= getArityMax();
    } 
    
    /**
     * Returns the String (name) representation of this procedure.
     */
    public String toString()
    {
        return "#[compound-procedure " + getIndex() + "]";
    }
    
    /**
     * Checks a given arity against this procedure and throws an 
     * <code>IllegalArgumentException</code> if it is invalid.
     * 
     * @param arity  an arity, or number of arguments, to test
     */
    protected void checkArity(int arity)
    {
        if (!isValidArity(arity)) {
            String fmt = "The procedure " + this + " has been called with %d " +
                " argument" + (arity == 1 ? "" : "s") + "; it requires %s.";
            String msg = String.format(fmt, arity, getArityMessage());
                
            throw new IllegalArgumentException(msg);
        }
    }
    
    /**
     * Returns a message describing the arity of this procedure. For 
     * example, "exactly <em>N</em> arguments" or "between <em>N</em> 
     * and <em>M</em> arguments."
     * 
     * @return  a description of the arity of this procedure
     */
    protected String getArityMessage()
    {
        if (getArityMin() == getArityMax()) {
            int arity = getArityMin();
            return "exactly " + arity + " argument" + (arity == 1 ? "" : "s");
        } else if (getArityMax() == Integer.MAX_VALUE) {
            int arity = getArityMin();
            return "at least " + arity + " argument" + (arity == 1 ? "" : "s");
        } else {
            int min = getArityMin(), max = getArityMax();
            return "between " + min + " and " + max + " arguments";
        }
    }
    
    /**
     * Add a symbol and register it with the global instance.
     * 
     * @param symbol  the symbol to add
     * @return  the symbol
     */
    protected SchemeSymbol addSymbol(String symbol)
    {
        SchemeSymbol sym = symbols.get(symbol);
        
        if (sym == null) {
            sym = SchemeSymbol.getInstance(symbol);
            symbols.put(symbol, sym);
        }
        
        return sym;
    }
    
    /**
     * Free all references this procedure holds to symbols.  This
     * should be called e.g., on exit().
     */
    protected void removeSymbols()
    {
        for (String symbol : symbols.keySet()) {
            SchemeSymbol.remove(symbol);
        }
        symbols.clear();
    }
}
